blob: c7eafb9ae2250fbc1684579c442f81517ef6b84d [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2000, 2003 IBM Corporation and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Common Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/cpl-v10.html
*
* Contributors:
* IBM Corporation - initial API and implementation
*******************************************************************************/
package org.eclipse.ui.views.markers.internal;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Comparator;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.NullProgressMonitor;
import org.eclipse.core.runtime.SubProgressMonitor;
import org.eclipse.swt.widgets.Table;
import org.eclipse.jface.viewers.TableViewer;
/**
* Contains a queue of changes to be applied to a particular TableViewer.
* This object is not multithread-friendly. The owning object must synchronize
* its access to this object such that no two threads attempt to access it
* simultaneously.
*/
final class DeferredQueue {
private static final String SETTING_CONTENTS = Messages.getString("DeferredQueue.setting_contents"); //$NON-NLS-1$
/**
* Maximum number of items to be added or removed from the viewer in a single update.
* (Will be used for an initially empty table)
*/
private static final int MAX_UPDATE = 40;
/**
* Minimum number of items to be added or removed from the viewer in a single update
* (Will be used once the table size reaches (GROWTH_LIMIT)
*/
private static final int MIN_UPDATE = 1;
/**
* Table size beyond which all updates will be approximately MIN_UPDATE
*/
private static final int GROWTH_LIMIT = 20000;
// Member variables
/**
* Current sort order (note that this object does the sorting -- not the viewer. The
* view contents are sorted by controlling their insertion order and position.)
*/
private Comparator sortOrder;
/**
* Set of items currently visible in the viewer
*/
private Set visibleItems = new HashSet();
/**
* Set of pending insertions that will occur at the end of the table (if the
* viewer is sorted, this will be changed into a sorted collection). This
* will not contain any items currently in the visibleItems set.
*/
private Set insertionsAtEnd = new HashSet();
/**
* Set of pending insertions that will occur in the middle of the table. This
* remains unsorted. Sorting will occur in the UI thread at insertion-time.
* This will not contain any items currently in the visibleItems set.
*/
private Set insertionsInMiddle = new HashSet();
/**
* List of pending removals. This is a subset of visibleItems.
*/
private Set pendingRemovals = new HashSet();
/**
* List of pending changes. This is a subset of visibleItems.
*/
private Set pendingChanges = new HashSet();
/**
* Pointer to the item currently at the end of the table, or null if
* the table is currently empty.
*/
private Object lastVisible = null;
/**
* True iff there may be items inthe insertionsInMiddle queue
* that can be moved to the insertionsAtEnd queue. This is set to
* true when lastVisible is reduced, and set to false when
* insertionsInMiddle is repartitioned.
*/
private boolean lastDirty = false;
/**
* pointer to the viewer being populated.
*/
private TableViewer viewer;
private boolean hasPendingChanges = false;
/**
* Constructs a new DeferredQueue.
*
* @param viewer
*/
public DeferredQueue(TableViewer viewer) {
this.viewer = viewer;
}
/**
* Returns the set of items currently visible in the viewer (read-only)
*
* @return the set of processed items
*/
public Object[] getVisibleItems() {
synchronized(visibleItems) {
return visibleItems.toArray();
}
}
/**
* Queues the given set of items to be refreshed in the viewer. If there
* are any items in the viewer (or its queues) that compare as equal(...)
* to one of these items, the new item will replace the old one. Should
* be run in a background thread.
*
* @param changes set if items to be refreshed in the viewer
*/
public void change(Collection changes) {
Iterator iter = changes.iterator();
while (iter.hasNext()) {
Object next = iter.next();
boolean isVisible = false;
synchronized(visibleItems) {
if (visibleItems.contains(next)) {
visibleItems.remove(next);
visibleItems.add(next);
pendingChanges.add(next);
isVisible = true;
hasPendingChanges = true;
}
}
if (!isVisible) {
if (insertionsInMiddle.contains(next)) {
insertionsInMiddle.remove(next);
insertionsInMiddle.add(next);
hasPendingChanges = true;
} else if (insertionsAtEnd.contains(next)) {
insertionsAtEnd.remove(next);
insertionsAtEnd.add(next);
hasPendingChanges = true;
}
}
}
}
/**
* Sets the desired contents of the viewer, enqueueing any additions and removals
* necessary such that the final contents of the viewer will be as specified.
* Should be run in a background thread. If the given montor is canceled (possibly
* in another thread), the operation will be aborted without modifying the queue.
*
* @param newPendingContents desired contents of the viewer
* @param mon progress monitor
*/
public void set(Collection newPendingContents, IProgressMonitor mon) {
mon.beginTask(SETTING_CONTENTS, 100);
// Set of items in newPendingContents that are not currently in visibleItems
Set newInsertions = new HashSet();
// New pendingRemovals queue
Set newPendingRemovals = new HashSet();
// New insertionsInMiddle queue
Set newInsertionsInMiddle = new HashSet();
// New insertionsAtEnd queue
Set newInsertionsAtEnd = newEndSet();
addWithProgress(newInsertions, newPendingContents, mon, 5);
synchronized(visibleItems) {
addWithProgress(newPendingRemovals, visibleItems, mon, 5);
removeWithProgress(newPendingRemovals, newInsertions, mon, 5);
removeWithProgress(newInsertions, visibleItems, mon, 5);
}
if (newInsertions.isEmpty() && newPendingRemovals.isEmpty()) {
return;
}
SubProgressMonitor sub = new SubProgressMonitor(mon, 80);
SortUtil.partition(newInsertionsInMiddle, newInsertionsAtEnd, newInsertionsAtEnd, newInsertions,
sortOrder, lastVisible, sub);
// Do nothing if the operation was cancelled.
if (mon.isCanceled()) {
mon.done();
return;
}
// Now we've computed everything. Apply all the computed changes.
hasPendingChanges = true;
lastDirty = false;
insertionsAtEnd.clear();
insertionsAtEnd = newInsertionsAtEnd;
insertionsInMiddle.clear();
insertionsInMiddle = newInsertionsInMiddle;
pendingRemovals.clear();
pendingRemovals = newPendingRemovals;
mon.done();
}
/**
* Applies the next set of changes to the table. Returns the number of items
* actually refreshed. Must run in the UI thread.
*
* @param maximumToChange maximum number of queued changes to apply
* @return the number of changes actually applied.
*/
private int nextChange(int maximumToChange) {
Collection result = SortUtil.removeFirst(pendingChanges, maximumToChange);
viewer.update(result.toArray(), null);
return result.size();
}
/**
* Applies the next set of removals from the table. Must run in the UI thread.
*
* @param maximumToRemove maximum number of items to remove from the table.
* @return the number of items actually removed.
*/
private int nextRemoval(int maximumToRemove) {
ArrayList result = new ArrayList(maximumToRemove);
int counter = maximumToRemove;
Iterator iter = pendingRemovals.iterator();
while (iter.hasNext() && counter > 0) {
Object next = iter.next();
result.add(next);
if (lastVisible != null && lastVisible.equals(next)) {
lastDirty = true;
}
iter.remove();
counter--;
}
synchronized(visibleItems) {
visibleItems.removeAll(result);
}
viewer.remove(result.toArray());
if (lastDirty) {
lastVisible = viewer.getElementAt(viewer.getTable().getItemCount() - 1);
}
return result.size();
}
/**
* Applies the next set of insertions into the middle of the queue.
*
* @param maximumToInsert
* @return
*/
private int nextInsertionInMiddle(int maximumToInsert) {
refreshQueues(new NullProgressMonitor());
Collection result = SortUtil.removeFirst(insertionsInMiddle, maximumToInsert);
synchronized(visibleItems) {
visibleItems.addAll(result);
}
// We manually compute the insertion position because setting a sorter on
// the viewer would force a refresh, which can be very slow with a large number
// of items.
Iterator iter = result.iterator();
while (iter.hasNext()) {
Object element = iter.next();
int insertionPos = getInsertPosition(element);
viewer.insert(element, insertionPos);
}
return result.size();
}
/**
* Applies the next insertion at the end of the table. Returns the number of
* items actually inserted.
*
* @param maximumToInsert maximum number of items to insert into the end
* of the table
* @return the number of items actually inserted into the table
*/
private int nextInsertionAtEnd(int maximumToInsert) {
refreshQueues(new NullProgressMonitor());
List result = new ArrayList(maximumToInsert);
Iterator iter = insertionsAtEnd.iterator();
for (int counter = 0; counter < maximumToInsert && iter.hasNext(); counter++) {
lastVisible = iter.next();
result.add(lastVisible);
iter.remove();
}
synchronized(visibleItems) {
visibleItems.addAll(result);
}
viewer.add(result.toArray());
return result.size();
}
/**
* Clears the set of visible items, and reinserts everything from scratch.
*/
public void reset() {
synchronized(visibleItems) {
visibleItems.removeAll(pendingRemovals);
insertionsInMiddle.addAll(visibleItems);
lastVisible = null;
lastDirty = true;
visibleItems.clear();
}
pendingRemovals.clear();
hasPendingChanges = true;
}
/**
* Returns true iff there are pending changes remaining to be applied.
*
* @return true iff there are pending changes to be applied to the table
*/
public boolean hasPendingChanges() {
return hasPendingChanges;
}
/**
* Returns an estimate of the work remaining (the result is meaningful with respect
* to the return value of nextUpdate())
*
* @return an estimate of the work remaining
*/
public int workRemaining() {
return pendingRemovals.size() + insertionsAtEnd.size() + insertionsInMiddle.size() + pendingChanges.size();
}
/**
*
* Cancels all pending insertions and removals.
*/
public void cancelPending() {
insertionsAtEnd.clear();
insertionsInMiddle.clear();
pendingRemovals.clear();
hasPendingChanges = false;
}
public void setComparator(Comparator c) {
if (sortOrder == c) {
return;
}
sortOrder = c;
lastVisible = null;
insertionsInMiddle.addAll(insertionsAtEnd);
insertionsAtEnd = newEndSet();
lastDirty = true;
reset();
}
/**
* Applies any deferred sorting.
* @param mon
*/
public void refreshQueues(IProgressMonitor mon) {
if (lastDirty) {
if (mon.isCanceled()) {
return;
}
HashSet newInsertionsInMiddle = new HashSet();
SortUtil.partition(newInsertionsInMiddle, insertionsAtEnd, insertionsAtEnd,
insertionsInMiddle, sortOrder, lastVisible, mon);
if (mon.isCanceled()) {
insertionsInMiddle.removeAll(insertionsAtEnd);
} else {
insertionsInMiddle = newInsertionsInMiddle;
lastDirty = false;
}
}
}
/**
* Determines the insertion position for the given element in the table. Note
* that this is essentially the same as TableViewer.getInsertPosition, but we
* need to do it here because if we set a sorter on the TableViewer, this will
* force a refresh of the table which can be extremely slow.
*
* @param element object whose insertion position is being computed.
* @return
*/
private int getInsertPosition(Object element) {
Table table = viewer.getTable();
if(sortOrder == null)
return table.getItemCount();
int count = table.getItemCount();
int min = 0, max = count - 1;
while (min <= max) {
int mid = (min + max) / 2;
Object data = table.getItem(mid).getData();
int compare = sortOrder.compare(data, element);
if (compare == 0) {
// find first item > element
while (compare == 0) {
++mid;
if (mid >= count) {
break;
}
data = table.getItem(mid).getData();
compare = sortOrder.compare(data, element);
}
return mid;
}
if (compare < 0)
min = mid + 1;
else
max = mid - 1;
}
return min;
}
/**
* Performs a single update to the viewer. Based on the contents of the pending* queues,
* items will either be removed, added, or refreshed in the viewer (in that order). This
* should only be called within a synchronized block, since the various queues shouldn't
* be modified during an update. This method is invoked repeatedly by jobs to gradually
* apply the pending changes.
*/
public int nextUpdate() {
int pendingRemovalsSize = pendingRemovals.size();
if (pendingRemovalsSize > 0) {
int currentSize = countVisibleItems();
// Determine if we should remove incrementally or rebuild the table from scratch.
int finalSize = currentSize - pendingRemovalsSize;
if (finalSize * finalSize * 2 <= currentSize * currentSize) {
// If we're removing enough items that it would be faster just to rebuild
// the table from scratch, do it that way.
reset();
getViewer().refresh();
return 0;
}
return nextRemoval(nextUpdateSize());
} else if (insertionsInMiddle.size() > 0) {
return nextInsertionInMiddle(nextUpdateSize());
} else if (insertionsAtEnd.size() > 0) {
return nextInsertionAtEnd(MAX_UPDATE);
} else if (pendingChanges.size() > 0) {
return nextChange(MAX_UPDATE);
}
hasPendingChanges = false;
return 0;
}
/**
* @return
*/
int countVisibleItems() {
synchronized(visibleItems) {
return visibleItems.size();
}
}
/**
* Returns the number of items that should be added or removed in the next
* incremental update. This is used for the operations whose runtime increases
* with the size of the visible items.
*
* @return the number of changes that should be applied in the next update
*/
private int nextUpdateSize() {
int size = GROWTH_LIMIT * (MAX_UPDATE - MIN_UPDATE)
/ (GROWTH_LIMIT + countVisibleItems()) + MIN_UPDATE;
return size;
}
/**
* Returns the TableViewer that is being populated.
*
* @return the TableViewer that is being modified.
*/
public TableViewer getViewer() {
return viewer;
}
/**
* Returns a new empty set to be used for insertionsAtEnd
*
* @return
*/
private Set newEndSet() {
if (sortOrder == null) {
return new HashSet();
} else {
return new TreeSet(sortOrder);
}
}
/**
* Removes all the given items from the target collection, and updates
* the given progress monitor by the given amount. If the monitor is cancelled,
* no changes are made to the target collection.
*
* @param target items will be removed from this collection
* @param toRemove items to be removed from the collection
* @param mon progress monitor to be updated
* @param worked amount to update the monitor
*/
private static void removeWithProgress(Collection target, Collection toRemove,
IProgressMonitor mon, int worked) {
if (mon.isCanceled()) {
return;
}
target.removeAll(toRemove);
mon.worked(worked);
}
/**
* Inserts all the given items into the target collection, and updates
* the progress monitor. If the monitor is cancelled, no changes will
* be made to the target.
*
* @param target collection into which items will be inserted
* @param toInsert items to be inserted into the collection
* @param mon progress monitor that will be updated
* @param worked amount to update the progress monitor
*/
private static void addWithProgress(Collection target, Collection toInsert,
IProgressMonitor mon, int worked) {
if (mon.isCanceled()) {
return;
}
target.addAll(toInsert);
mon.worked(worked);
}
/**
* @return
*/
public Comparator getSorter() {
return sortOrder;
}
}