/* * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * This code is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License version 2 only, as * published by the Free Software Foundation. Codename One designates this * particular file as subject to the "Classpath" exception as provided * by Oracle in the LICENSE file that accompanied this code. * * This code is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License * version 2 for more details (a copy is included in the LICENSE file that * accompanied this code). * * You should have received a copy of the GNU General Public License version * 2 along with this work; if not, write to the Free Software Foundation, * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. * * Please contact Codename One through http://www.codenameone.com/ if you * need additional information or have any questions. */ package com.codename1.ui.list; import com.codename1.ui.List; import com.codename1.ui.TextField; import com.codename1.ui.events.DataChangedListener; import com.codename1.ui.events.SelectionListener; import java.util.ArrayList; import java.util.Map; /** * This class allows filtering/sorting a list model dynamically using a text field * * @author Shai Almog */ public class FilterProxyListModel<T> implements ListModel<T>, DataChangedListener { private ListModel<T> underlying; private ArrayList<Integer> filter; private ArrayList<DataChangedListener> listeners = new ArrayList<DataChangedListener>(); private boolean startsWithMode; /** * The proxy is applied to the actual model and effectively hides it * @param underlying the "real" model for the list */ public FilterProxyListModel(ListModel<T> underlying) { this.underlying = underlying; underlying.addDataChangedListener(this); } private int getFilterOffset(int index) { if(filter == null) { return index; } if(filter.size() > index) { return filter.get(index).intValue(); } return -1; } /** * This method performs a sort of the list, to determine the sort order this class should be derived * and the compare() method should be overriden * @param ascending sort in ascending order */ public void sort(boolean ascending) { if(filter == null) { filterImpl(""); } Integer[] filterArray = new Integer[filter.size()]; Integer[] tempArray = new Integer[filter.size()]; for(int iter = 0 ; iter < filter.size() ; iter++) { filterArray[iter] = filter.get(iter); } System.arraycopy(filterArray, 0, tempArray, 0, filterArray.length); mergeSort(filterArray, tempArray, 0, filterArray.length, 0, ascending); for(int iter = 0 ; iter < filter.size() ; iter++) { filter.set(iter, tempArray[iter]); } dataChanged(DataChangedListener.CHANGED, -1); } private int compareObj(Object a, Object b, boolean ascending) { return compare(underlying.getItemAt(((Integer)a).intValue()), underlying.getItemAt(((Integer)b).intValue()), ascending); } /** * This method can be overriden by subclasses to allow sorting arbitrary objects within * the list, it follows the traditional contract of the compare method in Java * @param a first object * @param b second object * @param ascending direction of sort * @return 1, 0 or -1 to indicate the larger/smaller object */ protected int compare(Object a, Object b, boolean ascending) { String s1; String s2; if (a instanceof String) { s1 = (String) a; s2 = (String) b; } else { s1 = (String) ((Map) a).get("name"); s2 = (String) ((Map) b).get("name"); } s1 = s1.toUpperCase(); s2 = s2.toUpperCase(); if (ascending) { return s1.compareTo(s2); } else { return -s1.compareTo(s2); } } private void swap(Object[] dest, int offset1, int offset2) { Object val1 = dest[offset1]; Object val2 = dest[offset2]; dest[offset2] = val1; dest[offset1] = val2; } private void mergeSort(Object[] src, Object[] dest, int low, int high, int off, boolean ascending) { int length = high - low; // Insertion sort on smallest arrays if (length < 7) { for (int i=low; i<high; i++) for (int j=i; j>low && compareObj(dest[j-1], dest[j], ascending)>0; j--) swap(dest, j, j-1); return; } // Recursively sort halves of dest into src int destLow = low; int destHigh = high; low += off; high += off; int mid = (low + high) / 2; mergeSort(dest, src, low, mid, -off, ascending); mergeSort(dest, src, mid, high, -off, ascending); // If list is already sorted, just copy from src to dest. This is an // optimization that results in faster sorts for nearly ordered lists. if (compareObj(src[mid-1], src[mid], ascending) <= 0) { System.arraycopy(src, low, dest, destLow, length); return; } // Merge sorted halves (now in src) into dest for(int i = destLow, p = low, q = mid; i < destHigh; i++) { if (q >= high || p < mid && compareObj(src[p], src[q], ascending)<=0) dest[i] = src[p++]; else dest[i] = src[q++]; } } private int getUnderlyingOffset(int index) { if(filter == null) { return index; } return filter.indexOf(new Integer(index)); } /** * Returns the underlying model which is needed to perform mutations on the list. * @return the underlying model */ public ListModel getUnderlying() { return underlying; } private void filterImpl(String str) { filter = new ArrayList<Integer>(); str = str.toUpperCase(); for(int iter = 0 ; iter < underlying.getSize() ; iter++) { Object o = underlying.getItemAt(iter); if(o != null) { if(check(o, str)) { filter.add(new Integer(iter)); } } } } /** * Checks whether the filter condition is matched, receives an uppercase version of the * filter string to match against * @param o the object being compared * @param str the string * @return true if match is checked */ protected boolean check(Object o, String str) { if(o instanceof Map) { Map h = (Map)o; if(comp(h.get("name"), str)) { return true; } } else { String element = o.toString(); if(startsWithMode) { if(element.toUpperCase().startsWith(str)) { return true; } } else { if(element.toUpperCase().indexOf(str) > -1) { return true; } } } return false; } private boolean comp(Object val, String str) { if(startsWithMode) { return val != null && ((String)val).toUpperCase().startsWith(str); } return val != null && ((String)val).toUpperCase().indexOf(str) > -1; } /** * Filters the list based on the given string * @param str the string to filter the list by */ public void filter(String str) { filterImpl(str); dataChanged(DataChangedListener.CHANGED, -1); } /** * {@inheritDoc} */ public T getItemAt(int index) { return underlying.getItemAt(getFilterOffset(index)); } /** * {@inheritDoc} */ public int getSize() { if(filter == null) { return underlying.getSize(); } return filter.size(); } /** * {@inheritDoc} */ public int getSelectedIndex() { return Math.max(0, getUnderlyingOffset(underlying.getSelectedIndex())); } /** * {@inheritDoc} */ public void setSelectedIndex(int index) { if(index < 0) { underlying.setSelectedIndex(index); } else { underlying.setSelectedIndex(getFilterOffset(index)); } } /** * {@inheritDoc} */ public void addDataChangedListener(DataChangedListener l) { listeners.add(l); } /** * {@inheritDoc} */ public void removeDataChangedListener(DataChangedListener l) { listeners.remove(l); } /** * {@inheritDoc} */ public void addSelectionListener(SelectionListener l) { underlying.addSelectionListener(l); } /** * {@inheritDoc} */ public void removeSelectionListener(SelectionListener l) { underlying.removeSelectionListener(l); } /** * {@inheritDoc} */ public void addItem(T item) { underlying.addItem(item); } /** * {@inheritDoc} */ public void removeItem(int index) { underlying.removeItem(getFilterOffset(index)); } /** * {@inheritDoc} */ public void dataChanged(int type, int index) { if(index > -1) { index = getUnderlyingOffset(index); if(index < 0) { return; } } for(int iter = 0 ; iter < listeners.size() ; iter++) { listeners.get(iter).dataChanged(type, index); } } /** * Installs a search field on a list making sure the filter method is invoked properly */ public static void install(final TextField search, final List l) { search.addDataChangedListener(new DataChangedListener() { public void dataChanged(int type, int index) { FilterProxyListModel f; if(l.getModel() instanceof FilterProxyListModel) { f = (FilterProxyListModel)l.getModel(); } else { if(search.getText().length() == 0) { return; } f = new FilterProxyListModel(l.getModel()); l.setModel(f); } if(search.getText().length() == 0) { l.setModel(f.getUnderlying()); } else { f.filter(search.getText()); } } }); } /** * Installs a search field on a list making sure the filter method is invoked properly */ public static void install(final TextField search, final ContainerList l) { search.addDataChangedListener(new DataChangedListener() { public void dataChanged(int type, int index) { FilterProxyListModel f; if(l.getModel() instanceof FilterProxyListModel) { f = (FilterProxyListModel)l.getModel(); } else { if(search.getText().length() == 0) { return; } f = new FilterProxyListModel(l.getModel()); l.setModel(f); } if(search.getText().length() == 0) { l.setModel(f.getUnderlying()); } else { f.filter(search.getText()); } } }); } /** * When enabled this makes the filter check that the string starts with rather than within the index * @return the startsWithMode */ public boolean isStartsWithMode() { return startsWithMode; } /** * When enabled this makes the filter check that the string starts with rather than within the index * @param startsWithMode the startsWithMode to set */ public void setStartsWithMode(boolean startsWithMode) { this.startsWithMode = startsWithMode; } }