blob: f43a6dd9b8e27862d79bea4a6901b36f070b63ac [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2006, 2013 IBM Corporation and others.
*
* This program and the accompanying materials
* are made available under the terms of the Eclipse Public License 2.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* IBM Corporation - initial API and implementation
*******************************************************************************/
package org.eclipse.debug.internal.ui.viewers.model;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import org.eclipse.jface.viewers.TreePath;
/**
* Helper class to support filtering in virtual tree viewer.
* Translates indexes from viewer to model coordinate space (and visa versa).
* <p>
* This filter transform maintains a tree representing filtered elements in the
* viewer. The filtering is performed dynamically as elements are 'replaced' in the tree
* by a lazy tree content provider.
* </p>
* <p>
* This class not intended to be subclassed or instantiated. For internal use only.
* </p>
* @since 3.3
*/
public class FilterTransform {
private Node root = new Node();
class Node {
private int[] filteredIndexes = null;
private Object[] filteredElements = null;
private Map<Object, Node> children = null; // only set for parent nodes,
// indexed by child
Node() {
}
boolean addFilter(TreePath path, int childIndex, int pathIndex, Object filtered) {
if (pathIndex == path.getSegmentCount()) {
if (filteredIndexes == null) {
filteredIndexes = new int[]{childIndex};
filteredElements = new Object[]{filtered};
return true;
}
int location = Arrays.binarySearch(filteredIndexes, childIndex);
if (location >= 0) {
return false;
}
location = 0 - (location + 1);
int[] next = new int[filteredIndexes.length + 1];
Object[] filt = new Object[next.length];
if (location == 0) {
next[0] = childIndex;
filt[0] = filtered;
System.arraycopy(filteredIndexes, 0, next, 1, filteredIndexes.length);
System.arraycopy(filteredElements, 0, filt, 1, filteredElements.length);
} else if (location == filteredIndexes.length) {
next[filteredIndexes.length] = childIndex;
filt[filteredElements.length] = filtered;
System.arraycopy(filteredIndexes, 0, next, 0, filteredIndexes.length);
System.arraycopy(filteredElements, 0, filt, 0, filteredElements.length);
} else {
System.arraycopy(filteredIndexes, 0, next, 0, location);
System.arraycopy(filteredElements, 0, filt, 0, location);
next[location] = childIndex;
filt[location] = filtered;
System.arraycopy(filteredIndexes, location, next, location + 1, filteredIndexes.length - location);
System.arraycopy(filteredElements, location, filt, location + 1, filteredElements.length - location);
}
filteredIndexes = next;
filteredElements = filt;
return true;
}
if (children == null) {
children = new HashMap<>();
}
Object element = path.getSegment(pathIndex);
Node node = children.get(element);
if (node == null) {
node = new Node();
children.put(element, node);
}
return node.addFilter(path, childIndex, pathIndex + 1, filtered);
}
boolean clear(TreePath path, int pathIndex) {
if (pathIndex == path.getSegmentCount()) {
return true;
}
if (children == null) {
return false;
}
Object child = path.getSegment(pathIndex);
Node node = children.get(child);
if (node != null) {
if (node.clear(path, pathIndex + 1)) {
children.remove(child);
}
}
return children.isEmpty() && (filteredIndexes == null || filteredIndexes.length == 0);
}
boolean clear(TreePath path, int childIndex, int pathIndex) {
if (pathIndex == path.getSegmentCount()) {
if (filteredIndexes != null) {
int location = Arrays.binarySearch(filteredIndexes, childIndex);
if (location >= 0) {
// remove it
if (location == 0 && filteredIndexes.length == 1) {
filteredIndexes = null;
filteredElements = null;
return true;
}
int[] next = new int[filteredIndexes.length - 1];
Object[] filt = new Object[next.length];
if (location == 0) {
System.arraycopy(filteredIndexes, 1, next, 0, next.length);
System.arraycopy(filteredElements, 1, filt, 0, filt.length);
} else if (location == (filteredIndexes.length - 1)) {
System.arraycopy(filteredIndexes, 0, next, 0, location);
System.arraycopy(filteredElements, 0, filt, 0, location);
} else {
System.arraycopy(filteredIndexes, 0, next, 0, location);
System.arraycopy(filteredElements, 0, filt, 0, location);
System.arraycopy(filteredIndexes, location + 1, next, location, next.length - location);
System.arraycopy(filteredElements, location + 1, filt, location, filt.length - location);
}
filteredIndexes = next;
filteredElements = filt;
return false;
}
} else {
return false;
}
}
if (children == null) {
return false;
}
Object element = path.getSegment(pathIndex);
Node node = children.get(element);
if (node == null) {
return false;
}
boolean remove = node.clear(path, childIndex, pathIndex + 1);
if (remove) {
children.remove(element);
return filteredIndexes == null && children.isEmpty();
} else {
return false;
}
}
Node find(TreePath path, int pathIndex) {
if (pathIndex == path.getSegmentCount()) {
return this;
}
if (children == null) {
return null;
}
Object child = path.getSegment(pathIndex);
Node node = children.get(child);
if (node != null) {
return node.find(path, pathIndex + 1);
}
return null;
}
int viewToModel(int childIndex) {
if (filteredIndexes == null) {
return childIndex;
}
// If there are filtered children, then we want to find the
// (n+1)th missing number in the list of filtered indexes (missing
// entries are visible in the view). For example, if the request
// has asked for the model index corresponding to the 4th viewer
// index, then we want to find the 5th missing number in the
// filtered index sequence.
int count = -1; // count from 0, 1, 2...
int missingNumbers = 0; // how many numbers missing from the filtered index
int offset = 0; // offset into the filtered index
while (missingNumbers < (childIndex + 1)) {
count++;
if (offset < filteredIndexes.length) {
if (filteredIndexes[offset] == count) {
// not missing
offset++;
} else {
// missing
missingNumbers++;
}
} else {
missingNumbers++;
}
}
return count;
}
int modelToView(int childIndex) {
if (filteredIndexes == null) {
return childIndex;
}
int offset = 0;
for (int i = 0; i < filteredIndexes.length; i++) {
if (childIndex == filteredIndexes[i] ) {
return -1;
} else if (childIndex > filteredIndexes[i]) {
offset++;
} else {
break;
}
}
return childIndex - offset;
}
int modelToViewCount(int childCount) {
if (filteredIndexes == null) {
return childCount;
}
return childCount - filteredIndexes.length;
}
boolean isFiltered(int index) {
if (filteredIndexes != null) {
int location = Arrays.binarySearch(filteredIndexes, index);
return location >= 0;
}
return false;
}
int indexOfFilteredElement(Object element) {
if (filteredElements != null) {
for (int i = 0; i < filteredElements.length; i++) {
if (element.equals(filteredElements[i])) {
return filteredIndexes[i];
}
}
}
return -1;
}
/**
* Sets the child count for this element, trimming any filtered elements
* that were above this count.
*
* @param childCount new child count
*/
void setModelChildCount(int childCount) {
if (filteredIndexes != null) {
for (int i = 0; i < filteredIndexes.length; i++) {
if (filteredIndexes[i] >= childCount) {
// trim
if (i == 0) {
filteredIndexes = null;
// bug 200325 - filteredElements should have the same length
// as filteredIndexes
filteredElements = null;
return;
} else {
int[] temp = new int[i + 1];
System.arraycopy(filteredIndexes, 0, temp, 0, temp.length);
filteredIndexes = temp;
// bug 200325 - filteredElements should have the same length
// as filteredIndexes
Object[] temp2 = new Object[i + 1];
System.arraycopy(filteredElements, 0, temp2, 0, temp2.length);
filteredElements = temp2;
}
}
}
}
}
/**
* Updates filter index for a removed element at the given index
*
* @param index index at which an element was removed
*/
void removeElementFromFilters(int index) {
if (filteredIndexes != null) {
int location = Arrays.binarySearch(filteredIndexes, index);
if (location >= 0) {
// remove a filtered item
if (filteredIndexes.length == 1) {
// only filtered item
filteredIndexes = null;
filteredElements = null;
} else {
int[] next = new int[filteredIndexes.length - 1];
Object[] filt = new Object[next.length];
if (location == 0) {
// first
System.arraycopy(filteredIndexes, 1, next, 0, next.length);
System.arraycopy(filteredElements, 1, filt, 0, filt.length);
} else if (location == (filteredIndexes.length - 1)) {
// last
System.arraycopy(filteredIndexes, 0, next, 0, next.length);
System.arraycopy(filteredElements, 0, filt, 0, filt.length);
} else {
// middle
System.arraycopy(filteredIndexes, 0, next, 0, location);
System.arraycopy(filteredElements, 0, filt, 0, location);
System.arraycopy(filteredIndexes, location + 1, next, location, next.length - location);
System.arraycopy(filteredElements, location + 1, filt, location, filt.length - location);
}
filteredIndexes = next;
filteredElements = filt;
}
} else {
location = 0 - (location + 1);
}
if (filteredIndexes != null) {
// decrement remaining indexes
for (int i = location; i < filteredIndexes.length; i ++) {
filteredIndexes[i]--;
}
}
}
}
}
/**
* Filters the specified child of the given parent and returns
* whether the filter was added.
*
* @param parentPath path to parent element
* @param childIndex index of filtered child relative to parent (in model coordinates)
* @param element the filtered element
* @return whether the filter was added - returns <code>true</code> if the filter is
* added, and <code>false</code> if the index was already filtered
*/
public boolean addFilteredIndex(TreePath parentPath, int childIndex, Object element) {
return root.addFilter(parentPath, childIndex, 0, element);
}
/**
* Clears all filtered elements.
*/
public void clear() {
root = new Node();
}
/**
* Clears all filters in the subtree of the given element.
*
* @param path element path
*/
public void clear(TreePath path) {
root.clear(path, 0);
}
/**
* Clears the given filtered index of the specified parent. I.e.
* the child still exists, but is no longer filtered.
*
* @param parentPath parent path
* @param index index to clear
*/
public void clear(TreePath parentPath, int index) {
root.clear(parentPath, index, 0);
}
public int indexOfFilteredElement(TreePath parentPath, Object element) {
Node parentNode = root.find(parentPath, 0);
if (parentNode == null) {
return -1;
}
return parentNode.indexOfFilteredElement(element);
}
/**
* Translates and returns the given model index (raw index) into
* a view index (filtered index), or -1 if filtered.
*
* @param parentPath path to parent element
* @param childIndex index of child element in model space
* @return the given index in view coordinates, or -1 if filtered.
*/
public int modelToViewIndex(TreePath parentPath, int childIndex) {
Node parentNode = root.find(parentPath, 0);
if (parentNode == null) {
return childIndex;
}
return parentNode.modelToView(childIndex);
}
/**
* Translates and returns the given view index (filtered) into
* a model index (raw index).
*
* @param parentPath path to parent element
* @param childIndex index of child element in view space
* @return the given index in model coordinates
*/
public int viewToModelIndex(TreePath parentPath, int childIndex) {
Node parentNode = root.find(parentPath, 0);
if (parentNode == null) {
return childIndex;
}
return parentNode.viewToModel(childIndex);
}
/**
* Returns the number of children for the given parent, in the model.
*
* @param parentPath path to parent element
* @param viewCount number of children in the view
* @return number of children in the model
*/
public int viewToModelCount(TreePath parentPath, int viewCount) {
Node parentNode = root.find(parentPath, 0);
if (parentNode != null) {
if (parentNode.filteredIndexes != null) {
return viewCount + parentNode.filteredIndexes.length;
}
}
return viewCount;
}
/**
* Translates and returns the given model child count (raw) into
* a view count (filtered).
*
* @param parentPath path to parent element
* @param count child count in model space
* @return the given count in view coordinates
*/
public int modelToViewCount(TreePath parentPath, int count) {
Node parentNode = root.find(parentPath, 0);
if (parentNode == null) {
return count;
}
return parentNode.modelToViewCount(count);
}
/**
* Returns whether the given index of the specified parent is currently filtered.
*
* @param parentPath path to parent element
* @param index index of child element
* @return whether the child is currently filtered
*/
public boolean isFiltered(TreePath parentPath, int index) {
Node parentNode = root.find(parentPath, 0);
if (parentNode == null) {
return false;
}
return parentNode.isFiltered(index);
}
/**
* Returns filtered children of the given parent, or <code>null</code> if none.
*
* @param parentPath Path of parent element
* @return filtered children or <code>null</code>
*/
public int[] getFilteredChildren(TreePath parentPath) {
Node parentNode = root.find(parentPath, 0);
if (parentNode == null) {
return null;
}
return parentNode.filteredIndexes;
}
/**
* Clears any filters for the given parent above the given count.
*
* @param parentPath path to parent element
* @param childCount child count
*/
public void setModelChildCount(TreePath parentPath, int childCount) {
Node parentNode = root.find(parentPath, 0);
if (parentNode != null) {
parentNode.setModelChildCount(childCount);
}
}
/**
* The element at the given index has been removed from the parent. Update
* indexes.
*
* @param parentPath path to parent element
* @param index index of child element in model coordinates
*/
public void removeElementFromFilters(TreePath parentPath, int index) {
Node parentNode = root.find(parentPath, 0);
if (parentNode != null) {
parentNode.removeElementFromFilters(index);
}
}
/**
* The element has been removed from the parent. Update
* filtered indexes, in case it was a filtered object.
*
* @param parentPath path to parent element
* @param element removed element
* @return true if element was removed
*/
public boolean removeElementFromFilters(TreePath parentPath, Object element) {
Node parentNode = root.find(parentPath, 0);
if (parentNode != null) {
int index = parentNode.indexOfFilteredElement(element);
if (index >= 0) {
parentNode.removeElementFromFilters(index);
return true;
}
}
return false;
}
}