Mercurial > projects > dwt-addons
diff dwtx/jface/viewers/deferred/BackgroundContentProvider.d @ 10:b6c35faf97c8
Viewers
author | Frank Benoit <benoit@tionex.de> |
---|---|
date | Mon, 31 Mar 2008 00:47:19 +0200 |
parents | |
children | ea8ff534f622 |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/dwtx/jface/viewers/deferred/BackgroundContentProvider.d Mon Mar 31 00:47:19 2008 +0200 @@ -0,0 +1,593 @@ +/******************************************************************************* + * Copyright (c) 2005, 2006 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 + * Port to the D programming language: + * Frank Benoit <benoit@tionex.de> + *******************************************************************************/ +module dwtx.jface.viewers.deferred.BackgroundContentProvider; + +import dwtx.jface.viewers.deferred.ConcurrentTableUpdator; +import dwtx.jface.viewers.deferred.IConcurrentModel; +import dwtx.jface.viewers.deferred.IConcurrentModelListener; +import dwtx.jface.viewers.deferred.ChangeQueue; +import dwtx.jface.viewers.deferred.FastProgressReporter; +import dwtx.jface.viewers.deferred.AbstractVirtualTable; +import dwtx.jface.viewers.deferred.LazySortedCollection; + +import dwtx.core.runtime.Assert; +import dwtx.core.runtime.IProgressMonitor; +import dwtx.core.runtime.NullProgressMonitor; +import dwtx.jface.resource.JFaceResources; + +import dwtx.jface.viewers.AcceptAllFilter; +import dwtx.jface.viewers.IFilter; + +import dwt.dwthelper.utils; +import tango.core.Thread; + +/** + * Contains the algorithm for performing background sorting and filtering in a virtual + * table. This is the real implementation for <code>DeferredContentProvider</code>. + * However, this class will work with anything that implements <code>AbstractVirtualTable</code> + * rather than being tied to a <code>TableViewer</code>. + * + * <p> + * This is package visiblity since it currently only needs to be used in one place, + * but it could potentially be made public if there was a need to use the same background + * sorting algorithm for something other than a TableViewer. + * </p> + * + * <p> + * Information flow is like this: + * </p> + * <ol> + * <li>IConcurrentModel sends unordered elements to BackgroundContentProvider (in a background thread)</li> + * <li>BackgroundContentProvider sorts, filters, and sends element/index pairs to + * ConcurrentTableUpdator (in a background thread)</li> + * <li>ConcurrentTableUpdator batches the updates and sends them to an AbstractVirtualTable + * (in the UI thread)</li> + * </ol> + * + * <p> + * Internally, sorting is done using a <code>LazySortedCollection</code>. This data structure + * allows the content provider to locate and sort the visible range without fully sorting + * all elements in the table. It also supports fast cancellation, allowing the visible range + * to change in the middle of a sort without discarding partially-sorted information from + * the previous range. + * </p> + * + * @since 3.1 + */ +/* package */ final class BackgroundContentProvider { + + /** + * Sorting message string + */ + private static const String SORTING; + static this(){ + SORTING = JFaceResources.getString("Sorting"); //$NON-NLS-1$ + } + /** + * Table limit. -1 if unlimited + */ + private int limit = -1; + + /** + * Model that is currently providing input to this content provider. + */ + private IConcurrentModel model; + + /** + * Current sort order + */ + private /+volatile+/ Comparator sortOrder; + + /** + * True iff the content provider has + */ + private /+volatile+/ IFilter filter; + + /** + * Queued changes + */ + private ChangeQueue changeQueue; + + /** + * Listener that gets callbacks from the model + */ + private IConcurrentModelListener listener; + private void init_listener(){ + listener = new class IConcurrentModelListener { + /* (non-Javadoc) + * @see dwtx.jface.viewers.deferred.IConcurrentModelListener#add(java.lang.Object[]) + */ + public void add(Object[] added) { + this.outer.add(added); + } + + /* (non-Javadoc) + * @see dwtx.jface.viewers.deferred.IConcurrentModelListener#remove(java.lang.Object[]) + */ + public void remove(Object[] removed) { + this.outer.remove(removed); + } + + /* (non-Javadoc) + * @see dwtx.jface.viewers.deferred.IConcurrentModelListener#setContents(java.lang.Object[]) + */ + public void setContents(Object[] newContents) { + this.outer.setContents(newContents); + } + + /* (non-Javadoc) + * @see dwtx.jface.viewers.deferred.IConcurrentModelListener#update(java.lang.Object[]) + */ + public void update(Object[] changed) { + this.outer.update(changed); + } + + }; + } + /** + * Object that posts updates to the UI thread. Must synchronize on + * sortMutex when accessing. + */ + private ConcurrentTableUpdator updator; + + private IProgressMonitor sortingProgressMonitor; + private Thread sortThread = null; + + private /+volatile+/ FastProgressReporter sortMon; + + private /+volatile+/ ConcurrentTableUpdator.Range range; + + /** + * Creates a new background content provider + * + * @param table table that will receive updates + * @param model data source + * @param sortOrder initial sort order + */ + public this(AbstractVirtualTable table, + IConcurrentModel model, Comparator sortOrder) { + lock = new Object(); + filter = AcceptAllFilter.getInstance(); + changeQueue = new ChangeQueue(); + init_listener(); + sortingProgressMonitor = new NullProgressMonitor(); + sortMon = new FastProgressReporter(); + updator = new ConcurrentTableUpdator(table); + range = new ConcurrentTableUpdator.Range(0,0); + this.model = model; + this.sortOrder = sortOrder; + model.addListener(listener); + } + + /** + * Cleans up this content provider, detaches listeners, frees up memory, etc. + * Must be the last public method called on this object. + */ + public void dispose() { + cancelSortJob(); + updator.dispose(); + model.removeListener(listener); + } + + /** + * Force a refresh. Asks the model to re-send its complete contents. + */ + public void refresh() { + if (updator.isDisposed()) { + return; + } + model.requestUpdate(listener); + } + + /** + * Called from sortJob. Sorts the elements defined by sortStart and sortLength. + * Schedules a UI update when finished. + * + * @param mon monitor where progress will be reported + */ + private void doSort(IProgressMonitor mon) { + + // Workaround for some weirdness in the Jobs framework: if you cancel a monitor + // for a job that has ended and reschedule that same job, it will start + // the job with a monitor that is already cancelled. We can workaround this by + // removing all references to the progress monitor whenever the job terminates, + // but this would require additional synchronize blocks (which are slow) and more + // complexity. Instead, we just un-cancel the monitor at the start of each job. + mon.setCanceled(false); + + mon.beginTask(SORTING, 100); + + // Create a LazySortedCollection + Comparator order = sortOrder; + IFilter f = filter; + LazySortedCollection collection = new LazySortedCollection(order); + + // Fill it in with all existing known objects + Object[] knownObjects = updator.getKnownObjects(); + for (int i = 0; i < knownObjects.length; i++) { + Object object = knownObjects[i]; + if (object !is null) { + collection.add(object); + } + } + + bool dirty = false; + int prevSize = knownObjects.length; + updator.setTotalItems(prevSize); + + // Start processing changes + while(true) { + // If the sort order has changed, build a new LazySortedCollection with + // the new comparator + if (order !is sortOrder) { + dirty = true; + order = sortOrder; + // Copy all elements from the old collection to the new one + LazySortedCollection newCollection = new LazySortedCollection(order); + + Object[] items = collection.getItems(false); + for (int j = 0; j < items.length && order is sortOrder; j++) { + Object item = items[j]; + + newCollection.add(item); + } + + // If the sort order changed again, re-loop + if (order !is sortOrder) { + continue; + } + collection = newCollection; + continue; + } + + // If the filter has changed + if (f !is filter) { + dirty = true; + f = filter; + + Object[] items = collection.getItems(false); + + // Remove any items that don't pass the new filter + for (int j = 0; j < items.length && f is filter; j++) { + Object toTest = items[j]; + + if (!f.select(toTest)) { + collection.remove(toTest); + } + } + continue; + } + + // If there are pending changes, process one of them + if (!changeQueue.isEmpty()) { + dirty = true; + ChangeQueue.Change next = changeQueue.dequeue(); + + switch(next.getType()) { + case ChangeQueue.ADD: { + filteredAdd(collection, next.getElements(), f); + break; + } + case ChangeQueue.REMOVE: { + Object[] toRemove = next.getElements(); + + flush(toRemove, collection); + collection.removeAll(toRemove); + + break; + } + case ChangeQueue.UPDATE: { + Object[] items = next.getElements(); + + for (int i = 0; i < items.length; i++) { + Object item = items[i]; + + if (collection.contains(item)) { + // TODO: write a collection.update(...) method + collection.remove(item); + collection.add(item); + updator.clear(item); + } + } + + break; + } + case ChangeQueue.SET: { + Object[] items = next.getElements(); + collection.clear(); + filteredAdd(collection, items, f); + + break; + } + } + + continue; + } + + int totalElements = collection.size(); + if (limit !is -1) { + if (totalElements > limit) { + totalElements = limit; + } + } + + if (totalElements !is prevSize) { + prevSize = totalElements; + // Send the total items to the updator ASAP -- the user may want + // to scroll to a different section of the table, which would + // cause our sort range to change and cause this job to get cancelled. + updator.setTotalItems(totalElements); + dirty = true; + } + + // Terminate loop + if (!dirty) { + break; + } + + try { + ConcurrentTableUpdator.Range updateRange = updator.getVisibleRange(); + sortMon = new FastProgressReporter(); + range = updateRange; + int sortStart = updateRange.start; + int sortLength = updateRange.length; + + if (limit !is -1) { + collection.retainFirst(limit, sortMon); + } + + sortLength = Math.min(sortLength, totalElements - sortStart); + sortLength = Math.max(sortLength, 0); + + Object[] objectsOfInterest = new Object[sortLength]; + + collection.getRange(objectsOfInterest, sortStart, true, sortMon); + + // Send the new elements to the table + for (int i = 0; i < sortLength; i++) { + Object object = objectsOfInterest[i]; + updator.replace(object, sortStart + i); + } + + objectsOfInterest = new Object[collection.size()]; + + collection.getFirst(objectsOfInterest, true, sortMon); + + // Send the new elements to the table + for (int i = 0; i < totalElements; i++) { + Object object = objectsOfInterest[i]; + updator.replace(object, i); + } + + } catch (InterruptedException e) { + continue; + } + + dirty = false; + } + + mon.done(); + } + + /** + * @param collection + * @param toAdd + */ + private static void filteredAdd(LazySortedCollection collection, Object[] toAdd, IFilter filter) { + if (filter !is AcceptAllFilter.getInstance()) { + for (int i = 0; i < toAdd.length; i++) { + Object object = toAdd[i]; + + if (filter.select(object)) { + collection.add(object); + } + } + } else { + collection.addAll(toAdd); + } + } + + /** + * Sets the sort order for this content provider + * + * @param sorter sort order + */ + public void setSortOrder(Comparator sorter) { + Assert.isNotNull(cast(Object)sorter); + this.sortOrder = sorter; + sortMon.cancel(); + refresh(); + } + + /** + * Sets the filter for this content provider + * + * @param toSet filter to set + */ + public void setFilter(IFilter toSet) { + Assert.isNotNull(cast(Object)toSet); + this.filter = toSet; + sortMon.cancel(); + refresh(); + } + + /** + * Sets the maximum table size. Based on the current sort order, + * the table will be truncated if it grows beyond this size. + * Using a limit improves memory usage and performance, and is + * strongly recommended for large tables. + * + * @param limit maximum rows to show in the table or -1 if unbounded + */ + public void setLimit(int limit) { + this.limit = limit; + refresh(); + } + + /** + * Returns the maximum table size or -1 if unbounded + * + * @return the maximum number of rows in the table or -1 if unbounded + */ + public int getLimit() { + return limit; + } + + /** + * Checks if currently visible range has changed, and triggers and update + * and resort if necessary. Must be called in the UI thread, typically + * within a DWT.SetData callback. + * @param includeIndex the index that should be included in the visible range. + */ + public void checkVisibleRange(int includeIndex) { + updator.checkVisibleRange(includeIndex); + ConcurrentTableUpdator.Range newRange = updator.getVisibleRange(); + ConcurrentTableUpdator.Range oldRange = range; + + // If we're in the middle of processing an invalid range, cancel the sort + if (newRange.start !is oldRange.start || newRange.length !is oldRange.length) { + sortMon.cancel(); + } + } + + /** + * This lock protects the two bool variables sortThreadStarted and resortScheduled. + */ + private Object lock; + + /** + * true if the sort thread is running + */ + private bool sortThreadStarted = false; + + /** + * true if we need to sort + */ + private bool sortScheduled = false; + + private final class SortThread : Thread { + private this(String name) { + super(/+name+/); + } + + public void run() { + loop: while (true) { + synchronized (lock) { + sortScheduled = false; + } + try { + // this is the main work + doSort(sortingProgressMonitor); + } catch (Exception ex) { + // ignore + } + synchronized (lock) { + if (sortScheduled) { + continue loop; + } + sortThreadStarted = false; + break loop; + } + } + } + } + + /** + * Must be called whenever the model changes. Dirties this object and triggers a sort + * if necessary. + */ + private void makeDirty() { + synchronized (lock) { + sortMon.cancel(); + // request sorting + sortScheduled = true; + if (!sortThreadStarted) { + sortThreadStarted = true; + sortThread = new SortThread(SORTING); + sortThread.isDaemon = true; + sortThread.priority = sortThread.priority - 1; + sortThread.start(); + } + } + } + + /** + * Cancels any sort in progress. Note that we try to use the + * FastProgresReporter if possible since this is more responsive than + * cancelling the sort job. However, it is not a problem to cancel in both + * ways. + */ + private void cancelSortJob() { + sortMon.cancel(); + sortingProgressMonitor.setCanceled(true); + } + + /** + * Called when new elements are added to the model. + * + * @param toAdd + * newly added elements + */ + private void add(Object[] toAdd) { + changeQueue.enqueue(ChangeQueue.ADD, toAdd); + makeDirty(); + } + + /** + * Called with the complete contents of the model + * + * @param contents new contents of the model + */ + private void setContents(Object[] contents) { + changeQueue.enqueue(ChangeQueue.SET, contents); + makeDirty(); + } + + /** + * Called when elements are removed from the model + * + * @param toRemove elements removed from the model + */ + private void remove(Object[] toRemove) { + changeQueue.enqueue(ChangeQueue.REMOVE, toRemove); + makeDirty(); + refresh(); + } + + /** + * Notifies the updator that the given elements have changed + * + * @param toFlush changed elements + * @param collection collection of currently-known elements + */ + private void flush(Object[] toFlush, LazySortedCollection collection) { + for (int i = 0; i < toFlush.length; i++) { + Object item = toFlush[i]; + + if (collection.contains(item)) { + updator.clear(item); + } + } + } + + + /** + * Called when elements in the model change + * + * @param items changed items + */ + private void update(Object[] items) { + changeQueue.enqueue(ChangeQueue.UPDATE, items); + makeDirty(); + } +}